/**
 * @overview Define array function and extend array to array of tuples.
 */


/* Arrays generation */

/** Generate an array from a function.
 * @param {int} length
 * @param {function} f
 *  Function of the index i.<br />
 *  For extended array, function of index i and component k.
 * @param {int} [d]
 *  Dimension, for extended array.
 * @return {Array}
 *  The array of f(i) for i < length.<br />
 *  For extended array, the array of f(i, k) for i < length and k < d.
 */
Array.fromFunction = function (length, f, d) {
    'use strict';
    var errMsg = 'Array.fromFunction: ';
    // Check arguments
    if (typeof length !== 'number' || length < 0 || length % 1) {
        throw new Error(errMsg + 'length must be a positive integer');
    }
    if (typeof f !== 'function') {
        throw new Error(errMsg + 'second argument must be a function');
    }
    if (d !== undefined && (typeof d !== 'number' || d <= 0 || d % 1)) {
        throw new Error(errMsg + 'dimension must be a positive integer');
    }
    // Create and fill the array
    var i, N = length;
    var t = [];
    if (!d) {
        for (i = 0; i < N; i++) {
            t.push(f(i));
        }
    } else {
        N *= d;
        t.d = d;
        var k;
        for (i = 0; i < N; i++) {
            for (k = 0; k < d; k++) {
                t.push(f(i, k));
            }
        }
    }
    return t;
};

/** Generate a constant array.
 * @param {int} length
 * @param {any} [value = 0]
 * @param {int} [d]
 *  Dimension, for extended array.
 * @return {Array}
 *  An array filled with the given value.
 * @example
 *  // an array of size 10 containing zeros:
 *  var zeros = Array.constant(10);
 *  // an array of size 10 containing ones:
 *  var ones = Array.constant(10, 1);
 *  // an extended array of size 10 containing 3D point (0, 0, 0):
 *  var zeros3d = Array.constant(10, 0, 3);
 */
Array.constant = function (length, value, d) {
    'use strict';
    var errMsg = 'Array.constant: ';
    // Check arguments
    if (typeof length !== 'number' || length < 0 || length % 1) {
        throw new Error(errMsg + 'length must be a positive integer');
    }
    if (value === undefined) {
        value = 0;
    }
    if (d !== undefined && (typeof d !== 'number' || d <= 0 || d % 1)) {
        throw new Error(errMsg + 'dimension must be a positive integer');
    }
    // Create and fill the array
    var i, N = (d) ? length * d : length;
    var t = [];
    for (i = 0; i < N; i++) {
        t.push(value);
    }
    // Return
    if (d) {
        t.d = d;
    }
    return t;
};

/** Generate regularly spaced values in an interval.
 * @param {float|Array} [first = 0]
 *  First value.
 * @param {int} number
 *  Number of values.
 * @param {float|Array} [last = 1]
 *  Last value.
 * @return {Array}
 *  Array or extended array of values linearly spaced in range [first, last].
 * @example
 *  // 6 values in [0, 1]:
 *  var floating = Array.linearSpaced(6);
 *
 *  // 6 values in [0, 10], i.e. even numbers:
 *  var unsigned = Array.linearSpaced(6, 10);
 *
 *  // 11 values in [-10, 10]:
 *  var signed = Array.linearSpaced(-10, 11, 10);
 *
 *  // Extended array: 11 points on the segment from (0, 0) to (3, 1):
 *  var linear = Array.linearSpaced(11, [3, 1]);
 *
 *  // Extended array: 100 points on the segment from (-1, 0) to (1, 3):
 *  var affine = Array.linearSpaced([-1, 0], 100, [1, 3]);
 */
Array.linearSpaced = function (first, count, last) {
    'use strict';
    var errMsg = 'Array.linearSpaced: ';
    // Check arguments
    if (last === undefined) {
        last = (count === undefined) ? 1 : count;
        count = first;
        first = 0;
    }
    if (typeof count !== 'number' || count <= 0 || count % 1) {
        throw new Error(errMsg + 'number of point must be a positive integer');
    }
    // Initialize dimensions
    var d = (first.length || last.length) || (first.d || last.d);
    var initValue = function (value) {
        if (!d) { return value; }
        if (!value.length) { return Array.constant(d, value); }
        if (value.length === d && (!value.d || value.d === d)) { return value; }
        throw new Error(errMsg + 'inconsistent argument dimensions');
    };
    first = initValue(first);
    last = initValue(last);
    // Fill
    var i, k, N = count - 1;
    var t = [];
    if (!d) {
        for (i = 0; i <= N; i++) {
            t.push(first + (last - first) * i / N);
        }
    } else {
        t.d = d;
        for (i = 0; i <= N; i++) {
            for (k = 0; k < d; k++) {
                t.push(first[k] + (last[k] - first[k]) * i / N);
            }
        }
    }
    // Return
    return t;
};

/** Generate a grid of regularly spaced values in an interval.
 * @param {float|Array} [first = 0]
 *  First value.
 * @param {float|Array} step
 *  Step value.
 * @param {float|Array} [last = 1]
 *  Last value.
 * @return {Array}
 *  Array or extended array:<br />
 *  - contains: first + k*step for all positive integer k.<br />
 *  - included in the interval [first, last].
 */
Array.grid = function (first, step, last) {
    'use strict';
    var errMsg = 'Array.linearSpaced: ';
    // Check arguments
    if (last === undefined) {
        last = (step === undefined) ? 1 : step;
        step = first;
        first = 0;
    }
    // Initialize dimensions
    var d = (first.length || step.length || last.length)
        ||  (first.d || step.d || last.d);
    var initValue = function (value) {
        if (!d) { return value; }
        if (!value.length) { return Array.constant(d, value); }
        if (value.length === d && (!value.d || value.d === d)) { return value; }
        throw new Error(errMsg + 'inconsistent argument dimensions');
    };
    first = initValue(first);
    step = initValue(step);
    last = initValue(last);
    // Fill
    var x, t = [];
    if (!d) {
        for (x = first; x <= last; x += step) {
            t.push(x);
        }
    } else {
        t.d = d;
        x = first;
        var k, over = false;
        while (!over) {
            for (k = 0; k < d; k++) {
                t.push(x[k]);
                x[k] += step[k];
                if ((step[k] > 0 && x[k] > last[k]) || (step[k] < 0 && x[k] < last[k])) {
                    over = true;
                }
            }
        }
    }
    // Return
    return t;
};


/* Classic array */

/** Return a copy of the array or extended array.
 * @return {Array}
 *  A new array, containing the same elements as the original array.
 */
Array.prototype.copy = function () {
    'use strict';
    var i, N = this.length;
    var t = new this.constructor(N);
    for (i = 0; i < N; i++) {
        t[i] = this[i];
    }
    if (this.d) {
        t.d = this.d;
    }
    return t;
};

/** Apply a function to each element of (a copy of) the array.
 * @param {function} f
 *  Function to be applied, take one argument.
 * @return {Array}
 *  The array of f(t) for all t.
 */
Array.prototype.map = function (f) {
    'use strict';
    var i, N = this.length;
    var t = new this.constructor(N);
    for (i = 0; i < N; i++) {
        t[i] = f(this[i]);
    }
    return t;
};

/** Accumulate a value over each element of an (extended) array.
 * @param {function} f
 *  Function f(t, accu) to be called on each element t.
 * @param {T} [init = 0]
 *  Initial value of the accumulation.
 * @return {T|Array}
 *  The accumulated value.
 *  For extended array, the result is an array of size d.
 * @example
 *  var max = data.accumulate(Math.max, -Infinity);
 *  var sum = data.accumulate(function(a, b) { return a + b; });
 */
Array.prototype.accumulate = function (f, init) {
    'use strict';
    var errMsg = this.constructor.name + '.accumulate: ';
    if (typeof f !== 'function') {
        throw new Error(errMsg + 'first argument must be a function');
    }
    if (init === undefined) {
        init = 0;
    }
    var i, N = this.getLength();
    if (!this.d) {
        for (i = 0; i < N; i++) {
            init = f(this[i], init);
        }
    } else {
        var j, k, d = this.d;
        init = Array.constant(1, init, this.d);
        for (i = 0, j = 0; i < N; i++) {
            for (k = 0; k < d; k++, j++) {
                // j = i*d + k
                init[k] = f(this[j], init[k]);
            }
        }
    }
    return init;
};

/** Return a subset of the arary.
 * @param {Array|function} [which]
 *  Which element to select?<br />
 *  Can be:<br />
 *  - an array of indexes (select using element indices).<br />
 *  - a boolean array (select using a mask).<br />
 *  - a function returning true or false (select using a predicate function).
 * @return {Array}
 *  Without argument, return the indices of element which are true.<br />
 *  With an argument, return the array of selected elements.
 */
Array.prototype.select = function (which) {
    'use strict';
    var errMsg = this.constructor.name + '.select: ';
    // Get arguments
    var isArray = (which && typeof which !== 'function' && which.length !== undefined);
    var isBoolean = isArray && (typeof which[0] === 'boolean');
    var t, i, N = this.length;
    // Check some arguments
    if (which !== undefined && !isArray && typeof which !== 'function') {
        throw new Error(errMsg + 'argument must be an array or a function');
    }
    if (isArray && isBoolean && this.length !== which.length) {
        throw new Error(errMsg + 'the boolean mask must have the same size');
    }
    // Filter
    if (!which) {
        t = [];
        for (i = 0; i < N; i++) {
            if (this[i]) {
                t.push(i);
            }
        }
    } else if (!isArray) {
        t = [];
        for (i = 0; i < N; i++) {
            if (which(this[i])) {
                t.push(this[i]);
            }
        }
    } else if (isBoolean) {
        t = [];
        for (i = 0; i < N; i++) {
            if (which[i] === true) {
                t.push(this[i]);
            } else if (which[i] !== false) {
                throw new Error(errMsg + 'non-boolean value found in boolean mask');
            }
        }
    } else {
        var M = which.length;
        t = new this.constructor(M);
        for (i = 0; i < M; i++) {
            var k = which[i];
            if (!(0 <= k && k < N)) {
                throw new Error(errMsg + 'index out of bound');
            }
            if (k % 1) {
                throw new Error(errMsg + 'index must be integer values');
            }
            t[i] = this[k];
        }
    }
    // Return
    if (isArray && which.d) {
        t.d = which.d;
    }
    return t;
};


/* Array of arrays */

/** Is this array of arrays a rectangle?
 * @return {boolean}
 *  True iff the array contains arrays of the same length.
 */
Array.prototype.isRectangle = function () {
    'use strict';
    var i, N = this.length;
    if (!this[0] || this[0].length === undefined) {
        return false;
    }
    var d = this[0].length;
    for (i = 1; i < N; i++) {
        if (this[i].length !== d) {
            return false;
        }
    }
    return true;
};


/* Extended array */

/** Return the length of an extended array.
 * @return {int}
 *  Number of tuples in the array.
 */
Array.prototype.getLength = function () {
    'use strict';
    var d = this.d || 1;
    return Math.floor(this.length / d);
};

/** Is the length of an extended array valid?
 * @return {boolean}
 *  True iff the array contains a valid number of tuples,
 *  i.e. this.length === this.getLength() * this.d
 */
Array.prototype.isLengthValid = function () {
    'use strict';
    var d = this.d || 1;
    return (this.length % d === 0);
};


/* Both array of arrays and extended array */

/** Transpose (a copy of) an extended array or an array of arrays.
 * @return {Array}
 *  Transposed extended array or array of arrays.
 */
Array.prototype.transpose = function () {
    'use strict';
    var t, i, j, k, n, N, d;
    if (this.d) {
        d = this.d;
        N = this.getLength();
        t = new this.constructor(this.length);
        t.d = N;
        for (k = 0, j = 0; k < d; k++) {
            for (i = 0, n = k; i < N; i++, j++, n += d) {
                // j = k*N + i;
                // n = i*d + k;
                t[j] = this[n];
            }
        }
    } else if (this.isRectangle()) {
        N = this.length;
        d = this[0].length;
        t = new this.constructor(d);
        for (k = 0; k < d; k++) {
            var tk = new this[0].constructor(N);
            for (i = 0; i < N; i++) {
                tk[i] = this[i][k];
            }
            t[k] = tk;
        }
    } else {
        throw new Error('Array.transpose: '
                + 'can only be applied on an extended array '
                + 'or on an array of arrays with rectangular shape.');
    }
    return t;
};

/** Converttion between an array of arrays and an extended array.
 * @param {boolean} [toExtended]
 *  If true, convert to an extended array.<br />
 *  If false, convert to an array of arrays.<br />
 *  If undefined, convert from one to the other.
 * @return {Array}
 *  The array with the new shape.
 */
Array.prototype.convert2d = function (toExtended) {
    'use strict';
    // Check arguments
    var errMsg = this.constructor + '.convert2d: ';
    if (toExtended === undefined) {
        toExtended = !this.d;
    } else if (typeof toExtended !== 'boolean') {
        throw new Error(errMsg + 'parameter must be nothing or a boolean');
    }
    if (!this.isLengthValid()) {
        throw new Error(errMsg + 'invalid input length');
    }
    if ((toExtended && this.d) || (!toExtended && this.isRectangle())) {
        return this;
    }
    if (toExtended && !this.isRectangle()) {
        throw new Error(errMsg + 'the shape of the array must be a rectangle');
    }
    // Convert
    var t, i, j, k, N, d;
    if (toExtended) {
        N = this.length;
        d = this[0].length;
        t = new this[0].constructor(N * d);
        t.d = d;
        for (i = 0, j = 0; i < N; i++) {
            for (k = 0; k < d; k++, j++) {
                // j = i*d + k
                t[j] = this[i][k];
            }
        }
    } else {
        d = this.d;
        N = this.getLength();
        t = [];
        for (i = 0, j = 0; i < N; i++) {
            var ti = new this.constructor(d);
            for (k = 0; k < d; k++, j++) {
                // j = i*d + k
                ti[k] = this[j];
            }
            t.push(ti);
        }
    }
    // Return
    return t;
};